# -*- coding: utf-8 -*-
"""
保险销售预测系统
功能：基于健康保险用户数据预测是否会购买车辆保险
版本：1.0
日期：2025年2月
"""

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score, roc_curve
import joblib
import os
# ----------------------------
# 配置项设置
# ----------------------------
# 设置中文显示
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号
# 文件路径配置
TRAIN_PATH = 'train.csv'
TEST_PATH = 'test.csv'
SUBMISSION_PATH = 'sample_submission.csv'
MODEL_SAVE_PATH = 'insurance_model.pkl'
PREDICTIONS_SAVE_PATH = 'insurance_predictions.csv'

# 特征配置
CATEGORICAL_FEATURES = ['Gender', 'Vehicle_Age', 'Vehicle_Damage']  # 类别特征
NUMERICAL_FEATURES = ['Age', 'Region_Code', 'Annual_Premium',
                      'Policy_Sales_Channel', 'Vintage']  # 数值特征
BINARY_FEATURES = ['Driving_License', 'Previously_Insured']  # 二进制特征
TARGET = 'Response'  # 目标变量


# ----------------------------
# 核心功能函数
# ----------------------------
def load_data():
    """
    加载训练集、测试集和提交模板数据
    返回：三个数据集DataFrame，如果加载失败返回None
    """
    try:
        print("=" * 50)
        print("1. 开始加载数据...")

        # 读取数据（使用逗号分隔，因为你的数据实际是CSV格式）
        train_df = pd.read_csv(TRAIN_PATH)
        test_df = pd.read_csv(TEST_PATH)
        submission_df = pd.read_csv(SUBMISSION_PATH)

        # 显示数据基本信息
        print(f"✅ 训练集加载完成，共 {train_df.shape[0]} 行，{train_df.shape[1]} 列")
        print(f"✅ 测试集加载完成，共 {test_df.shape[0]} 行，{test_df.shape[1]} 列")
        print(f"✅ 提交模板加载完成，共 {submission_df.shape[0]} 行，{submission_df.shape[1]} 列")

        # 显示前2行数据，方便确认
        print("\n训练集前2行示例：")
        print(train_df.head(2).to_string())  # 使用to_string()确保完整显示
        return train_df, test_df, submission_df

    except FileNotFoundError as e:
        print(f"❌ 数据加载失败：找不到文件 - {e}")
        print(f"   请确认 {TRAIN_PATH}、{TEST_PATH}、{SUBMISSION_PATH} 这三个文件是否在当前目录")
        return None, None, None
    except Exception as e:
        print(f"❌ 数据加载出错：{e}")
        return None, None, None


def explore_data(train_df):
    """
    探索性数据分析，生成数据分布图表
    参数：训练集DataFrame
    """
    print("\n" + "=" * 50)
    print("2. 开始数据分析...")

    # 创建保存图表的文件夹
    if not os.path.exists('charts'):
        os.makedirs('charts')

    # 1. 目标变量分布分析
    print("\n分析目标变量(是否购买车辆保险)的分布：")
    if TARGET not in train_df.columns:
        print(f"❌ 错误：训练数据中找不到目标变量 '{TARGET}'")
        print(f"   可用列名：{list(train_df.columns)}")
        return

    response_count = train_df[TARGET].value_counts()
    response_ratio = train_df[TARGET].value_counts(normalize=True) * 100
    print(f"   未购买(0): {response_count[0]} 人，占比 {response_ratio[0]:.2f}%")
    print(f"   已购买(1): {response_count[1]} 人，占比 {response_ratio[1]:.2f}%")

    # 绘制目标变量分布图
    plt.figure(figsize=(8, 5))
    sns.countplot(x=TARGET, data=train_df)
    plt.title('用户购买车辆保险的分布情况')
    plt.xlabel('是否购买 (0=否, 1=是)')
    plt.ylabel('用户数量')
    plt.savefig('charts/目标变量分布.png')
    plt.close()
    print("   ✅ 目标变量分布图已保存到 charts/目标变量分布.png")

    # 2. 性别与购买意愿的关系
    plt.figure(figsize=(8, 5))
    gender_relation = pd.crosstab(train_df['Gender'], train_df[TARGET], normalize='index') * 100
    gender_relation.plot(kind='bar', stacked=True)
    plt.title('不同性别的购买意愿')
    plt.ylabel('比例 (%)')
    plt.savefig('charts/性别与购买意愿.png')
    plt.close()
    print("   ✅ 性别分析图已保存到 charts/性别与购买意愿.png")

    # 3. 年龄与购买意愿的关系
    plt.figure(figsize=(10, 6))
    sns.histplot(data=train_df, x='Age', hue=TARGET, multiple='stack', bins=20)
    plt.title('年龄分布与购买意愿')
    plt.xlabel('年龄')
    plt.ylabel('用户数量')
    plt.savefig('charts/年龄与购买意愿.png')
    plt.close()
    print("   ✅ 年龄分析图已保存到 charts/年龄与购买意愿.png")

    # 4. 车辆使用年限与购买意愿的关系
    plt.figure(figsize=(10, 6))
    vehicle_age_relation = pd.crosstab(train_df['Vehicle_Age'], train_df[TARGET], normalize='index') * 100
    vehicle_age_relation.plot(kind='bar', stacked=True)
    plt.title('车辆使用年限与购买意愿')
    plt.ylabel('比例 (%)')
    plt.savefig('charts/车辆年限与购买意愿.png')
    plt.close()
    print("   ✅ 车辆年限分析图已保存到 charts/车辆年限与购买意愿.png")

    print("✅ 数据分析完成，所有图表已保存到 charts 文件夹")


def build_model(train_df):
    """
    构建并训练预测模型，包含三轮优化
    参数：训练集DataFrame
    返回：训练好的模型
    """
    print("\n" + "=" * 50)
    print("3. 开始构建模型...")

    # 检查必要的列是否存在
    missing_features = [f for f in CATEGORICAL_FEATURES + NUMERICAL_FEATURES + BINARY_FEATURES if
                        f not in train_df.columns]
    if missing_features:
        print(f"❌ 错误：训练数据中缺少以下特征列：{missing_features}")
        print(f"   可用列名：{list(train_df.columns)}")
        return None

    # 准备特征和目标变量
    X = train_df[CATEGORICAL_FEATURES + NUMERICAL_FEATURES + BINARY_FEATURES]
    y = train_df[TARGET]

    # 划分训练集和验证集（80%训练，20%验证）
    X_train, X_val, y_train, y_val = train_test_split(
        X, y, test_size=0.2, random_state=42, stratify=y
    )
    print(f"   训练集: {X_train.shape[0]} 样本，验证集: {X_val.shape[0]} 样本")

    # 定义数据预处理管道
    # 类别特征：进行独热编码
    categorical_transformer = Pipeline(steps=[
        ('onehot', OneHotEncoder(drop='first', handle_unknown='ignore'))
    ])

    # 数值特征：进行标准化
    numerical_transformer = Pipeline(steps=[
        ('scaler', StandardScaler())
    ])

    # 合并所有预处理步骤
    preprocessor = ColumnTransformer(
        transformers=[
            ('cat', categorical_transformer, CATEGORICAL_FEATURES),
            ('num', numerical_transformer, NUMERICAL_FEATURES),
            ('bin', 'passthrough', BINARY_FEATURES)  # 二进制特征直接使用
        ])

    # ----------------------
    # 第一轮优化：基础模型
    # ----------------------
    print("\n第一轮优化：基础随机森林模型")
    model_v1 = Pipeline(steps=[
        ('preprocessor', preprocessor),
        ('classifier', RandomForestClassifier(
            n_estimators=100,  # 树的数量
            max_depth=8,  # 树的最大深度
            random_state=42,
            n_jobs=-1  # 使用所有CPU核心
        ))
    ])

    model_v1.fit(X_train, y_train)
    y_pred_v1 = model_v1.predict_proba(X_val)[:, 1]
    auc_v1 = roc_auc_score(y_val, y_pred_v1)
    print(f"   第一轮模型AUC得分: {auc_v1:.4f}")

    # ----------------------
    # 第二轮优化：增加树的数量和深度
    # ----------------------
    print("\n第二轮优化：增加树的数量和深度")
    model_v2 = Pipeline(steps=[
        ('preprocessor', preprocessor),
        ('classifier', RandomForestClassifier(
            n_estimators=200,  # 增加树的数量
            max_depth=12,  # 增加树的深度
            random_state=42,
            n_jobs=-1
        ))
    ])

    model_v2.fit(X_train, y_train)
    y_pred_v2 = model_v2.predict_proba(X_val)[:, 1]
    auc_v2 = roc_auc_score(y_val, y_pred_v2)
    print(f"   第二轮模型AUC得分: {auc_v2:.4f}")
    print(f"   相比第一轮提升: {auc_v2 - auc_v1:.4f}")

    # ----------------------
    # 第三轮优化：调整分裂参数和正则化
    # ----------------------
    print("\n第三轮优化：调整分裂参数和正则化")
    model_v3 = Pipeline(steps=[
        ('preprocessor', preprocessor),
        ('classifier', RandomForestClassifier(
            n_estimators=200,
            max_depth=12,
            min_samples_split=5,  # 减少分裂所需样本数
            min_samples_leaf=2,  # 增加叶节点最小样本数
            class_weight='balanced',  # 处理类别不平衡
            random_state=42,
            n_jobs=-1
        ))
    ])

    model_v3.fit(X_train, y_train)
    y_pred_v3 = model_v3.predict_proba(X_val)[:, 1]
    auc_v3 = roc_auc_score(y_val, y_pred_v3)
    print(f"   第三轮模型AUC得分: {auc_v3:.4f}")
    print(f"   相比第二轮提升: {auc_v3 - auc_v2:.4f}")

    # 绘制最终模型的ROC曲线
    fpr, tpr, _ = roc_curve(y_val, y_pred_v3)
    plt.figure(figsize=(8, 8))
    plt.plot(fpr, tpr, label=f'ROC曲线 (AUC = {auc_v3:.4f})')
    plt.plot([0, 1], [0, 1], 'k--')  # 随机猜测的基准线
    plt.xlabel('假正例率')
    plt.ylabel('真正例率')
    plt.title('模型ROC曲线')
    plt.legend()
    plt.savefig('charts/模型ROC曲线.png')
    plt.close()

    # 保存最终模型
    joblib.dump(model_v3, MODEL_SAVE_PATH)
    print(f"\n✅ 最佳模型已保存到 {MODEL_SAVE_PATH}")
    print(f"   最终模型AUC得分: {auc_v3:.4f}")

    return model_v3


def make_predictions(model, test_df, submission_df):
    """
    使用训练好的模型对测试集进行预测，并生成提交文件
    参数：模型、测试集、提交模板
    """
    print("\n" + "=" * 50)
    print("4. 开始预测并生成结果...")

    # 检查测试集是否包含所有需要的特征列
    missing_features = [f for f in CATEGORICAL_FEATURES + NUMERICAL_FEATURES + BINARY_FEATURES if
                        f not in test_df.columns]
    if missing_features:
        print(f"❌ 错误：测试数据中缺少以下特征列：{missing_features}")
        print(f"   可用列名：{list(test_df.columns)}")
        return

    # 提取测试集特征
    X_test = test_df[CATEGORICAL_FEATURES + NUMERICAL_FEATURES + BINARY_FEATURES]

    # 预测购买概率
    test_df['购买概率'] = model.predict_proba(X_test)[:, 1]
    # 预测是否购买（以0.5为阈值）
    test_df['预测结果'] = (test_df['购买概率'] >= 0.5).astype(int)

    # 保存详细预测结果（包含所有特征和预测值）
    test_df.to_csv('详细预测结果.csv', index=False, encoding='utf-8-sig')
    print(f"✅ 详细预测结果已保存到 详细预测结果.csv")

    # 生成符合提交格式的文件
    submission_result = submission_df.copy()
    # 将预测结果合并到提交模板中（通过id匹配）
    submission_result = submission_result.merge(
        test_df[['id', '预测结果']],
        on='id',
        how='left'
    )
    # 填充可能的缺失值（如果有的话）
    submission_result['Response'] = submission_result['预测结果'].fillna(0).astype(int)
    submission_result = submission_result.drop('预测结果', axis=1)

    # 保存提交文件
    submission_result.to_csv(PREDICTIONS_SAVE_PATH, index=False)
    print(f"✅ 提交格式的预测结果已保存到 {PREDICTIONS_SAVE_PATH}")

    # 显示预测结果分布
    print("\n预测结果分布：")
    pred_count = test_df['预测结果'].value_counts()
    pred_ratio = test_df['预测结果'].value_counts(normalize=True) * 100
    print(f"   预测不购买(0): {pred_count[0]} 人，占比 {pred_ratio[0]:.2f}%")
    print(f"   预测购买(1): {pred_count[1]} 人，占比 {pred_ratio[1]:.2f}%")


# ----------------------------
# 主程序入口
# ----------------------------
def main():
    """主函数，按步骤执行整个流程"""
    print("===== 保险销售预测系统 =====")

    # 步骤1：加载数据
    train_df, test_df, submission_df = load_data()
    if train_df is None:  # 如果数据加载失败，直接退出
        return

    # 步骤2：数据分析
    explore_data(train_df)

    # 步骤3：构建和训练模型
    model = build_model(train_df)
    if model is None:  # 如果模型构建失败，退出
        return

    # 步骤4：预测并生成结果
    make_predictions(model, test_df, submission_df)

    print("\n" + "=" * 50)
    print("🎉 所有任务完成！")
    print("   1. 数据分析图表在 charts 文件夹")
    print("   2. 训练好的模型在 insurance_model.pkl")
    print("   3. 详细预测结果在 详细预测结果.csv")
    print("   4. 提交格式结果在 insurance_predictions.csv")


if __name__ == "__main__":
    main()